
#ifndef ROPUFU_STATISTIC_GENERALIZED_CUSUM_HPP_INCLUDED
#define ROPUFU_STATISTIC_GENERALIZED_CUSUM_HPP_INCLUDED

#include "../information/information_at_most.hpp"
#include "../information/information_exact.hpp"
#include "../information/information_none.hpp"
#include "../more_math.hpp"
#include "../window/window_none.hpp"
#include "cusum.hpp"
#include "parallel_max_cusum.hpp"

#include <array>
#include <bitset>
#include <cmath>
#include <cstdint>
#include <ostream>
#include <string>
#include <type_traits>
#include <vector>

namespace ropufu
{
    // TODO: only implemented for independent streams.
    namespace statistic
    {
        namespace detail
        {
            template <typename t_information_type>
            struct generalized_cusum_do_timeline 
                : public std::false_type
            {
            };

            template <std::size_t t_count_streams, std::size_t t_count_affected>
            struct generalized_cusum_do_timeline<information_exact<t_count_streams, t_count_affected>> 
                : public std::true_type
            {
                static double execute(double x, double log_weight)
                {
                    return x + log_weight;
                }

                static double fuse(std::array<double, t_count_streams>& local_stat)
                {
                    double statistic = 0.0;
                    more_math::top_sort(local_stat, t_count_affected);
                    for (std::size_t k = 0; k < t_count_affected; k++) statistic += local_stat[(t_count_streams - 1) - k];
                    return statistic;
                }
            };

            template <std::size_t t_count_streams, std::size_t t_count_affected>
            struct generalized_cusum_do_timeline<information_at_most<t_count_streams, t_count_affected>> 
                : public std::true_type
            {
                static double execute(double x, double log_weight)
                {
                    x += log_weight;
                    return x > 0.0 ? x : 0.0;
                }

                static double fuse(std::array<double, t_count_streams>& local_stat)
                {
                    double statistic = 0.0;
                    more_math::top_sort(local_stat, t_count_affected);
                    for (std::size_t k = 0; k < t_count_affected; k++) statistic += local_stat[(t_count_streams - 1) - k];
                    return statistic;
                }
            };

            template <std::size_t t_count_streams>
            struct generalized_cusum_do_timeline<information_none<t_count_streams>>
                : public generalized_cusum_do_timeline<information_at_most<t_count_streams, t_count_streams>>
            {
            };
        }

        template <std::size_t t_count_streams, 
            typename t_information_type = void, typename t_window_type = void,
            bool t_do_timeline = detail::generalized_cusum_do_timeline<t_information_type>::value>
        struct generalized_cusum
            : public parallel_max_cusum<t_count_streams, t_information_type, t_window_type>
        {
            static const std::size_t count_streams = t_count_streams;
            typedef typename switch_void<t_information_type, information_none<count_streams>>::type information_type;
            typedef typename switch_void<t_window_type, window_none<count_streams>>::type window_type;
            typedef generalized_cusum<count_streams, t_information_type, t_window_type, t_do_timeline> type;

            using parallel_max_cusum<t_count_streams, t_information_type, t_window_type>::parallel_max_cusum;
        };

        template <std::size_t t_count_streams, typename t_information_type, typename t_window_type>
        struct generalized_cusum<t_count_streams, t_information_type, t_window_type, true>
        {
            static const std::size_t count_streams = t_count_streams;
            typedef typename switch_void<t_information_type, information_none<count_streams>>::type information_type;
            typedef typename switch_void<t_window_type, window_none<count_streams>>::type window_type;
            typedef generalized_cusum<count_streams, t_information_type, t_window_type, true> type;

            typedef detail::generalized_cusum_do_timeline<information_type> information_helper_type;

        private:
            information_type m_prior_knowledge;
            window_type m_window;
            double m_current_statistic = 0.0;
            // TODO: implement non-unit weights (right now p_{\cA} = p^{|\cA |} / \sum p^{|\cB |} with p = 1).
            double m_log_weight = 0.0;
            bool m_has_reset = false;

            generalized_cusum() = delete;

        public:
            explicit generalized_cusum(const information_type& prior_knowledge, const window_type& window)
                : m_prior_knowledge(prior_knowledge), m_window(window)
            {
            }

            bool has_reset() const
            {
                return this->m_has_reset;
            }

            void reset()
            {
                this->m_window.reset();
                this->m_current_statistic = 0.0;
                this->m_has_reset = false;
            }

            double observe(const std::array<double, count_streams>& instantaneous_loglikelihood)
            {
                this->m_current_statistic = 0.0;
                this->m_window.observe(instantaneous_loglikelihood);

                std::size_t sz = this->m_window.get_depth();
                this->m_has_reset = sz == 0; // TODO: implement as <window_type> property.

                for (std::size_t s = 0; s < sz; s++)
                {
                    std::array<double, count_streams> local_stat;
                    for (std::size_t k = 0; k < count_streams; k++)
                    {
                        local_stat[k] = information_helper_type::execute(this->m_window.get_history_at(k, s), this->m_log_weight);
                    }
                    double m = information_helper_type::fuse(local_stat);
                    if (m > this->m_current_statistic) this->m_current_statistic = m;
                }
                // TODO: implement non-unit weights (right now p_{\cA} = p^{|\cA |} / \sum p^{|\cB |} with p = 1).
                double log_weight_norm = std::log(static_cast<double>(this->m_prior_knowledge.cardinality()));
                this->m_current_statistic -= log_weight_norm;
                return this->m_current_statistic;
            }

            // Returns the complete loglikelihood ratio.
            const std::array<double, count_streams>& complete() const
            {
                return this->m_window.complete();
            }

            // Returns the accumulated loglikelihood ratio.
            const std::array<double, count_streams>& accumulated() const
            {
                return this->m_window.accumulated();
            }

            friend std::ostream& operator <<(std::ostream& os, const type& that)
            {
                return os << "glr_cusum_" << that.m_prior_knowledge << "_" << that.m_window;
            }
        };
    }
}

#endif // ROPUFU_STATISTIC_GENERALIZED_CUSUM_HPP_INCLUDED
